﻿// <copyright file="Articles.cs" company="Microsoft Corporation">
// Copyright (c) 2008 All Right Reserved
// </copyright>
// <author>Michael S. Scherotter</author>
// <email>mischero@microsoft.com</email>
// <date>2009-03-06</date>
// <summary>New York Times Articles search API</summary>

namespace NYTimes
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.IO;
    using System.IO.IsolatedStorage;
    using System.Json;
    using System.Linq;
    using System.Net;
    using System.Runtime.Serialization;
    using System.Text;
    using System.Windows;
    using System.Windows.Media.Imaging;
    using NYTimes.Core;
    using NYTimes.Data;
    using Synergist;

    /// <summary>
    /// New York Times Articles Search API
    /// </summary>
    /// <remarks>For more information, see <a href="http://developer.nytimes.com/docs/article_search_api">this page</a>.
    /// This is available on Silverlight but not on WPF or Windows Phone 7 Series.
    /// <list type="table">
    /// <listheader>
    /// <term>Application Resource Key</term>
    /// <description>Key to add to application resources</description>
    /// </listheader>
    /// <item>
    /// <term>
    /// NYTArticlesKey
    /// </term>
    /// <description>Articles API Key</description>
    /// </item>
    /// <item>
    /// <term>
    /// NYTArticlesQPS
    /// </term>
    /// <description><a href="bce0a4af-c5fb-40d5-8ada-6f3267a87221.htm">Rate Limiting</a> (default 10 QPS)</description>
    /// </item>
    /// </list>
    /// </remarks>
    public class Articles : INotifyPropertyChanged
    {
        #region Fields
        /// <summary>
        /// The Base URI
        /// </summary>
        private const string BaseUri = "http://api.nytimes.com/svc/search/v1/article";

        /// <summary>
        /// Application App Key
        /// </summary>
        private const string NYTArticlesKey = "NYTArticlesKey";

        /// <summary>
        /// number of results per call
        /// </summary>
        private const int ResultsPerCall = 10;

        /// <summary>
        /// 10 QPS default rate limiting
        /// </summary>
        private static RateLimiting limiting = new RateLimiting("NYTArticles", 10);

        /// <summary>
        /// the Articles API Key
        /// </summary>
        private string apiKey;

        /// <summary>
        /// the status
        /// </summary>
        private string status;

        /// <summary>
        /// the begin date
        /// </summary>
        private DateTime beginDate;

        /// <summary>
        /// the facets to request
        /// </summary>
        private string facets;

        /// <summary>
        /// the fields to request
        /// </summary>
        private string fields;

        /// <summary>
        /// the current offset
        /// </summary>
        private int offset;

        /// <summary>
        /// The Async web client
        /// </summary>
        private WebClient client;

        /// <summary>
        /// the query string
        /// </summary>
        private string query;

        /// <summary>
        /// is the CLR class loading?
        /// </summary>
        private bool isLoading;

        /// <summary>
        /// the end date
        /// </summary>
        private DateTime endDate;
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the Articles class.
        /// </summary>
        public Articles()
        {
            this.ResultsToGet = 50;

            this.Results = new ObservableCollection<Article>();
            this.FacetsReturned = new Dictionary<string, FacetCollection>();

            if (Application.Current.Resources.Contains(NYTArticlesKey))
            {
                this.ApiKey = Application.Current.Resources[NYTArticlesKey] as string;
            }

            this.Rank = RankOrder.Newest;
            this.FieldNames = new string[]
            {
                "abstract",
                "author",
                "body",
                "byline",
                "classifiers_facet",
                "column_facet",
                "date_facet",
                "day_of_week_facet",
                "des_facet",
                "desk_facet",
                "fee",
                "geo_facet",
                "lead_paragraph",
                "material_type_facet",
                "multimedia",
                "nytd_byline",
                "nytd_des_facet",
                "nytd_geo_facet",
                "nytd_lead_paragraph",
                "nytd_org_facet",
                "nytd_per_facet",
                "nytd_section_facet",
                "nytd_title",
                "nytd_works_mentioned_facet",
                "org_facet",
                "page_facet",
                "per_facet",
                "publication_day",
                "publication_month",
                "publication_year",
                "related_multimedia",
                "section_page_facet",
                "small_image",
                "small_image_url",
                "small_image_height",
                "small_image_width",
                "source_facet",
                "text",
                "title",
                "tokens",
                "url",
                "word_count",
                "works_mentioned_facet"
            };

            this.FacetNames = from field in this.FieldNames
                              where field.EndsWith("_facet", StringComparison.OrdinalIgnoreCase)
                              select field;

            string json;

            if (DesignerProperties.IsInDesignTool)
            {
                json = NYTimes.Core.SampleData.GetSampleData("Articles.json");

                if (!string.IsNullOrEmpty(json))
                {
                    using (var reader = new StringReader(json))
                    {
                        this.ReadJSON(reader);
                    }
                }
            }
            else if (IsolatedStorageSettings.ApplicationSettings.TryGetValue("NYTimes.Articles.Cache", out json))
            {
                try
                {
                    using (var reader = new StringReader(json))
                    {
                        this.ReadJSON(reader);
                    }
                }
                catch (SerializationException se)
                {
                    System.Diagnostics.Debug.WriteLine(se.Message);

                    IsolatedStorageSettings.ApplicationSettings.Remove("NYTimes.Articles.Cache");
                }
            }
        }
        #endregion

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Property changed event handler
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Properties
        /// <summary>
        /// Gets a value indicating whether the results are loading
        /// </summary>
        public bool IsLoading 
        {
            get
            {
                return this.isLoading;
            }

            private set
            {
                if (this.isLoading != value)
                {
                    this.isLoading = value;

                    if (this.PropertyChanged != null)
                    {
                        this.PropertyChanged(this, new PropertyChangedEventArgs("IsLoading"));
                    }
                }
            }
        }

        /// <summary>
        /// Gets the logo for Billboard
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1822", Justification = "This is exposed to a Silverlight application.")]
        public BitmapImage Logo
        {
            get
            {
                var streamResource = Application.GetResourceStream(
    new Uri("NYTimesSilverlightKit;component/Images/Logo.png", UriKind.Relative));

                var image = new BitmapImage();

                image.SetSource(streamResource.Stream);

                return image;
            }
        }

        /// <summary>
        /// Gets or sets the number of results to get
        /// </summary>
        [Description("The total number of results to get.")]
        public int ResultsToGet { get; set; }

        /// <summary>
        /// Gets or sets the Fields to request
        /// </summary>
        [Description("The fields to request (separated by commas)")]
        public string Fields 
        { 
            get 
            {
                return this.fields;
            }

            set
            {
                if (this.fields != value)
                {
                    this.fields = value;

                    this.Offset = 0;
                }
            }
        }

        /// <summary>
        /// Gets or sets the Facets to request (separated by commas, 5 maximum)
        /// </summary>
        [Description("Gets or sets the facets to return (separated by commas, 5 maximum)")]
        public string Facets 
        { 
            get
            {
                return this.facets;
            }

            set
            {
                if (this.facets != value)
                {
                    this.facets = value;

                    this.Offset = 0;
                }
            }
        }

        /// <summary>
        /// Gets the facets returned
        /// </summary>
        public IDictionary<string, FacetCollection> FacetsReturned { get; private set; }

        /// <summary>
        /// Gets or sets the query
        /// </summary>
        [Description("Gets or sets the query term(s) to look for in New York Times Articles")]
        public string Query
        {
            get
            {
                return this.query;
            }

            set
            {
                if (this.query != value)
                {
                    this.query = value;

                    if (this.PropertyChanged != null)
                    {
                        this.PropertyChanged(this, new PropertyChangedEventArgs("Query"));
                    }

                    this.Offset = 0;
                }
            }
        }

        /// <summary>
        /// Gets or sets the api key
        /// </summary>
        [Description("The New York Times Article Search API Key")]
        public string ApiKey
        {
            get
            {
                return this.apiKey;
            }

            set
            {
                this.apiKey = value;

                this.Offset = 0;
            }
        }

        /// <summary>
        /// Gets the results offset
        /// </summary>
        public int Offset
        {
            get 
            { 
                return this.offset; 
            }

            private set
            {
                this.offset = value;

                if (this.PropertyChanged != null)
                {
                    this.PropertyChanged(this, new PropertyChangedEventArgs("Offset"));
                }

                if (!DesignerProperties.IsInDesignTool)
                {
                    if (value == 0)
                    {
                        Deployment.Current.Dispatcher.BeginInvoke(new Action(delegate
                            {
                                this.FacetsReturned.Clear();
                                this.Results.Clear();
                            }));
                    }

                    if (this.IsAutoFetch)
                    {
                        this.GetArticles();
                    }
                }
            }
        }

        /// <summary>
        /// Gets or sets the begin date >= 1/1/1981
        /// </summary>
        [Description("The Begin Date for the search"),
        TypeConverter(typeof(DateTimeTypeConverter))]
        public DateTime BeginDate
        {
            get 
            { 
                return this.beginDate; 
            }

            set
            {
                var minDate = new DateTime(1981, 1, 1);

                if (value < minDate)
                {
                    this.beginDate = minDate;
                }
                else
                {
                    this.beginDate = value;
                }

                this.Offset = 0;
            }
        }

        /// <summary>
        /// Gets or sets the end date
        /// </summary>
        [Description("The Begin Date for the search"), 
        TypeConverter(typeof(DateTimeTypeConverter))]
        public DateTime EndDate 
        {
            get
            {
                return this.endDate;
            }

            set
            {
                this.endDate = value;

                this.Offset = 0;
            }
        }

        /// <summary>
        /// Gets or sets the rank order for the results
        /// </summary>
        [Description("The rank for the result order")]
        public RankOrder Rank { get; set; }

        /// <summary>
        /// Gets the results of the search
        /// </summary>
        public ObservableCollection<Article> Results { get; private set; }

        /// <summary>
        /// Gets the tokens returned
        /// </summary>
        public IEnumerable<string> Tokens { get; private set; }

        /// <summary>
        /// Gets the available field names
        /// </summary>
        public IEnumerable<string> FieldNames { get; private set; }

        /// <summary>
        /// Gets the facet names to query
        /// </summary>
        public IEnumerable<string> FacetNames { get; private set; }

        /// <summary>
        /// Gets the total number of results
        /// </summary>
        public int Total { get; private set; }

        /// <summary>
        /// Gets or sets a value indicating whether the Articles object automaticall 
        /// fetches articles when any input value changes
        /// </summary>
        public bool IsAutoFetch { get; set; }

        /// <summary>
        /// Gets or sets any status message
        /// </summary>
        public string Status
        {
            get
            {
                return this.status;
            }

            set
            {
                if (this.status != value)
                {
                    this.status = value;

                    this.NotifyPropertyChanged("Status");
                }
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Get the articles
        /// </summary>
        public void GetArticles()
        {
            this.GetArticles(this.Query, new DownloadStringCompletedEventHandler(this.OnArticlesReturned));
        }

        /// <summary>
        /// Get the articles
        /// </summary>
        /// <param name="queryString">the query to send to the service</param>
        /// <param name="handler">the download completed handler</param>
        public void GetArticles(string queryString, DownloadStringCompletedEventHandler handler)
        {
            if (string.IsNullOrEmpty(queryString))
            {
                return;
            }

            Deployment.Current.Dispatcher.BeginInvoke(delegate
            {
                this.IsLoading = true;
            });

            if (this.client != null)
            {
                System.Diagnostics.Debug.WriteLine("Cancelling pending request.");

                limiting.Cancel(this.client);

                this.client.CancelAsync();
            }

            var uriString = this.BuildUri(queryString);

            System.Diagnostics.Debug.WriteLine(uriString);

            this.client = new WebClient();

            var address = new Uri(uriString, UriKind.Absolute);

            this.client.DownloadStringCompleted += handler;

            limiting.MakeWebRequest(this.client, address);

            ////this.client.DownloadStringAsync(address);
        }

        /// <summary>
        /// Parse the results from the articles request
        /// </summary>
        /// <param name="e">the download completed event args.</param>
        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "This is a complex function.")]
        public void ParseResults(DownloadStringCompletedEventArgs e)
        {
            if (e == null)
            {
                throw new ArgumentNullException("e");
            }

            if (e.Error != null)
            {
                this.Status = Resources.UnableToAccessNYTArtilces;

                System.Diagnostics.Debug.WriteLine("Failed to get New York Times Articles: {0}", e.Error.Message);

                this.IsLoading = false;

                return;
            }

            using (var reader = new StringReader(e.Result))
            {
                this.ReadJSON(reader);
            }

            IsolatedStorageSettings.ApplicationSettings["NYTimes.Articles.Cache"] = e.Result;
        }

        #endregion

        #region Implementation

        /// <summary>
        /// Parse a date returned from the service
        /// </summary>
        /// <param name="text">a date in YYYYMMDD format</param>
        /// <returns> a DateTime</returns>
        internal static DateTime ParseDate(string text)
        {
            return new DateTime(
                int.Parse(text.Substring(0, 4), CultureInfo.InvariantCulture), 
                int.Parse(text.Substring(4, 2), CultureInfo.InvariantCulture), 
                int.Parse(text.Substring(6, 2), CultureInfo.InvariantCulture));
        }

        #endregion

        /// <summary>
        /// Read the JSON search results
        /// </summary>
        /// <param name="reader">the text reader</param>
        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "This is a complex function.")]
        private void ReadJSON(TextReader reader)
        {
            var response = JsonValue.Load(reader) as JsonObject;

            string offsetValue = response["offset"].ToString();

            System.Diagnostics.Debug.WriteLine("Reading JSON Offset {0}", offsetValue);

            this.Tokens = from token in response["tokens"] as JsonArray
                          select token.ToString().Substring(1, token.ToString().Length - 2);

            var total = response["total"].ToString();

            this.Total = int.Parse(total, CultureInfo.InvariantCulture);

            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs("Total"));
            }

            var queryResults = response["results"] as JsonArray;

            this.ParseFacets(response);

            var articles = from article in queryResults
                           select new Article()
                           {
                               Abstract = article.ContainsKey("abstract") ? article["abstract"] : null,
                               Body = article.ContainsKey("body") ? Platform.HtmlDecode(article["body"]) + "..." : null,
                               Byline = article.ContainsKey("byline") ? article["byline"] : null,
                               ColumnFacet = article.ContainsKey("column_facet") ? article["column_facet"].ToString() : null,
                               Comments = article.ContainsKey("comments") ? article["comments"] == "Y" : false,
                               Date = article.ContainsKey("date") ? ParseDate(article["date"]) : DateTime.MinValue,
                               DBpediaResource = article.ContainsKey("dbpedia_resource") ? article["dbpedia_resource"] : null,
                               DBpediaResourceUrl = article.ContainsKey("dbpedia_resource_url") ? new Uri(article["dbpedia_resource_url"], UriKind.Absolute) : null,
                               DescriptionFacet = article.ContainsKey("des_facet") ? from facet in article["des_facet"] as JsonArray
                                                                                     select facet.ToString() : null,
                               DeskFacet = article.ContainsKey("desk_facet") ? article["desk_facet"] : null,
                               Fee = article.ContainsKey("fee") ? bool.Parse(article["fee"]) : false,
                               GeoFacet = article.ContainsKey("geo_facet") ? from facet in article["geo_facet"] as JsonArray
                                                                             select facet.ToString() : null,
                               MultimediaFeatures = article.ContainsKey("multimedia") ? new Collection<Multimedia>((from multimedia in article["multimedia"] as JsonArray
                                                                                                                    select new Multimedia()
                                                                                                                    {
                                                                                                                        Headline = multimedia["headline"],
                                                                                                                        Height = multimedia.ContainsKey("height") ? int.Parse(multimedia["height"], CultureInfo.InvariantCulture) : 0,
                                                                                                                        Summary = multimedia["summary"],
                                                                                                                        MediaType = multimedia["type"],
                                                                                                                        Url = new Uri(multimedia["url"], UriKind.Absolute),
                                                                                                                        Width = multimedia.ContainsKey("width") ? int.Parse(multimedia["width"], CultureInfo.InvariantCulture) : 0,
                                                                                                                    }).ToList()) : null,
                               SectionFacet = article.ContainsKey("nytd_section_facet") ? string.Join(
                                    ", ", 
                                    (from facet in article["nytd_section_facet"] as JsonArray 
                                     select facet.ToString().Trim('"')).ToArray()) : null,
                               PageFacet = article.ContainsKey("page_facet") ? article["page_facet"] : null,
                               PersonFacet = article.ContainsKey("per_facet") ? from facet in article["per_facet"] as JsonArray
                                                                                select facet.ToString() : null,
                               SmallImageHeight = article.ContainsKey("small_image_height") ? int.Parse(article["small_image_height"], CultureInfo.InvariantCulture) : 0,
                               SmallImageWidth = article.ContainsKey("small_image_width") ? int.Parse(article["small_image_width"], CultureInfo.InvariantCulture) : 0,
                               SmallImageUrl = article.ContainsKey("small_image_url") ? new Uri(article["small_image_url"], UriKind.Absolute) : null,
                               SourceFacet = article.ContainsKey("source_facet") ? article["source_facet"] : null,
                               Title = article.ContainsKey("title") ? article["title"] : null,
                               Url = article.ContainsKey("url") ? new Uri(article["url"], UriKind.Absolute) : null
                           };

            foreach (var article in articles)
            {
                this.Results.Add(article);

                if (this.Results.Count >= this.ResultsToGet)
                {
                    break;
                }
            }

            int currentOffset = int.Parse(offsetValue.Substring(1, offsetValue.Length - 2), CultureInfo.InvariantCulture);

            if ((currentOffset * ResultsPerCall) + ResultsPerCall < this.ResultsToGet && (currentOffset * ResultsPerCall) + ResultsPerCall < this.Total)
            {
                this.Offset++;
            }
            else
            {
                this.IsLoading = false;
            }

            this.Status = string.Format(CultureInfo.CurrentCulture, "{0} articles returnd on {1:d}.", this.Results.Count, DateTime.Now);
        }

        /// <summary>
        /// Parse the facets returned from the Service
        /// </summary>
        /// <param name="response">the JSON response</param>
        private void ParseFacets(JsonObject response)
        {
            if (response.ContainsKey("facets"))
            {
                var facetsReturned = response["facets"] as JsonObject;

                this.FacetsReturned.Clear();

                foreach (var facet in facetsReturned)
                {
                    var elementArray = facet.Value as JsonArray;

                    var elements = from element in elementArray
                                   select new Facet
                                   {
                                       Term = element["term"],
                                       Count = element["count"]
                                   };

                    this.FacetsReturned[facet.Key] = new FacetCollection { Facets = elements };

                    if (this.PropertyChanged != null)
                    {
                        this.PropertyChanged(this, new PropertyChangedEventArgs("FacetsReturned"));
                    }
                }
            }
        }

        /// <summary>
        /// Build the URI
        /// </summary>
        /// <param name="queryString">the query string</param>
        /// <returns>a URI string for the REST request</returns>
        private string BuildUri(string queryString)
        {
            if (string.IsNullOrEmpty(this.ApiKey))
            {
                return null;
            }

            var uriString = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}?query={1}&api-key={2}", BaseUri, queryString, this.ApiKey);

            var builder = new StringBuilder(uriString);

            if (!string.IsNullOrEmpty(this.Fields))
            {
                builder.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "&fields={0}", this.Fields.Replace(" ", string.Empty));
            }

            if (!string.IsNullOrEmpty(this.Facets) && this.Offset == 0)
            {
                // Only request facets for the first call
                builder.AppendFormat(
                    System.Globalization.CultureInfo.InvariantCulture,
                    "&facets={0}",
                   this.Facets.Replace(" ", string.Empty));
            }

            if (this.BeginDate != DateTime.MinValue && this.EndDate != DateTime.MinValue)
            {
                builder.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, "&begin_date={0:yyyyMMdd}&end_date={1:yyyyMMdd}", this.BeginDate, this.EndDate);
            }

            if (this.Offset > 0)
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "&offset={0}", this.Offset);
            }

            uriString = builder.ToString();

            return uriString;
        }

        /// <summary>
        /// fill the Results with the parsed results
        /// </summary>
        /// <param name="sender">the web client</param>
        /// <param name="e">the download string completed event arguments</param>
        private void OnArticlesReturned(object sender, DownloadStringCompletedEventArgs e)
        {
            this.client = null;

            if (e.Cancelled)
            {
                return;
            }

            try
            {
                this.ParseResults(e);
            }
            catch (SerializationException se)
            {
                System.Diagnostics.Debug.WriteLine("Error parsing article data from NYTimes.com: {0}", se.Message);

                this.Status = Resources.UnableToAccessNYTArtilces;

                this.IsLoading = false;
            }
        }

        private void NotifyPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}
